Toast 组件的计时器测试
Toast 组件内部使用 setTimeout 实现延时隐藏功能。这类涉及异步计时的组件是单元测试中的典型难点 — 等待真实定时器会严重拖慢测试速度,需要使用 Vitest 的 Fake Timers 来模拟时间流逝。
Toast 组件结构
Toast 组件接收以下 Props:
interface ToastProps {
text: string // 显示文本
time: number // 延时(毫秒)
modelValue: boolean // v-model 控制显示/隐藏
}
typescript
核心逻辑:当 modelValue 为 true 时显示,经过 time 毫秒后自动触发 update:modelValue 事件设为 false。
基本渲染测试
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { mount, VueWrapper } from '@vue/test-utils'
import Toast from '../Toast.vue'
describe('Toast 组件测试', () => {
const props = {
text: 'hello toast',
time: 2500
}
it('未设置 modelValue 时,Toast 不显示', () => {
const wrapper = mount(Toast, {
props
})
// Transition 包裹的内部 div 长度应为 0
expect(wrapper.findAll('div')).toHaveLength(0)
expect(wrapper).toBeTruthy()
})
})
typescript
测试 v-model 显示
使用 setProps 动态设置 modelValue:
it('设置 modelValue 为 true 时,显示 Toast 文本', async () => {
const wrapper = mount(Toast, { props })
await wrapper.setProps({ modelValue: true })
const html = wrapper.html()
expect(html).toContain('hello toast')
})
typescript
vi.useFakeTimers — 模拟计时器
Vitest 提供 vi 工具对象来处理各种测试场景,其中 Fake Timers 是测试 setTimeout 的核心工具。
基本用法
import { vi } from 'vitest'
// 启用模拟计时器(替代所有 setTimeout、setInterval、Date 等)
vi.useFakeTimers()
// 恢复真实计时器
vi.useRealTimers()
typescript
- `vi.useFakeTimers()` 通常放在 `beforeEach` 中,在每次测试前启用
vi.useRealTimers()通常放在afterEach中,在每次测试后恢复
检查计时器数量
it('设置 show 后,应有计时器运行', async () => {
vi.useFakeTimers()
const wrapper = mount(Toast, { props })
await wrapper.setProps({ modelValue: true })
// 此时应该有一个 timer 等待执行
const count = vi.getTimerCount()
expect(count).toBe(1)
vi.useRealTimers()
})
typescript
vi.runAllTimers — 执行所有计时器
it('计时器执行完毕后,触发 update:modelValue', async () => {
vi.useFakeTimers()
const wrapper = mount(Toast, { props })
await wrapper.setProps({ modelValue: true })
// 执行所有待处理的计时器回调
vi.runAllTimers()
// timer 已清空
expect(vi.getTimerCount()).toBe(0)
vi.useRealTimers()
})
typescript
测试 emit 事件
验证组件是否正确触发了 update:modelValue 事件及其参数:
it('触发 update:modelValue 事件', async () => {
vi.useFakeTimers()
const wrapper = mount(Toast, { props })
await wrapper.setProps({ modelValue: true })
vi.runAllTimers()
// 验证事件被触发
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
// 验证事件参数为 false
const emittedEvents = wrapper.emitted('update:modelValue')
expect(emittedEvents![0]).toEqual([false])
vi.useRealTimers()
})
typescript
vi.advanceTimersByTime — 精确控制时间
advanceTimersByTime(ms) 允许精确推进指定毫秒数的计时器,适合测试边界条件:
it('部分时间未到达时,timer 不应执行完成', async () => {
vi.useFakeTimers()
const wrapper = mount(Toast, { props })
await wrapper.setProps({ modelValue: true })
// 只推进 time - 1 毫秒(差 1ms 未完成)
vi.advanceTimersByTime(props.time - 1)
// timer 尚未完成
expect(vi.getTimerCount()).not.toBe(0)
// 推进剩余时间
vi.runAllTimers()
expect(vi.getTimerCount()).toBe(0)
vi.useRealTimers()
})
typescript
这个测试验证了 props.time 是否被正确设置为 setTimeout 的延迟时间。
生命周期组织
完整的 Toast 组件测试推荐使用以下生命周期组织方式:
describe('Toast 组件测试', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
// 测试用例...
})
typescript
Vitest API 速览
核心 API
| API | 用途 |
|---|---|
describe(name, fn) | 定义测试套件 |
it(name, fn) | 定义单个测试用例 |
expect(value) | 创建断言 |
vi.useFakeTimers() | 启用模拟计时器 |
vi.useRealTimers() | 恢复真实计时器 |
vi.runAllTimers() | 立即执行所有待处理的定时器回调 |
vi.advanceTimersByTime(ms) | 推进指定毫秒数 |
vi.getTimerCount() | 获取当前待处理的计时器数量 |
Vue Test Utils 常用方法
| 方法 | 说明 |
|---|---|
mount(Component, options) | 完整挂载(包含子组件) |
shallowMount(Component, options) | 浅挂载(子组件被 stub 替代) |
wrapper.setProps(props) | 异步更新组件 Props |
wrapper.html() | 获取渲染的 HTML 字符串 |
wrapper.emitted() | 获取组件触发的所有事件 |
wrapper.findAll(selector) | 查找所有匹配元素,返回数组 |
wrapper.vm | 访问 Vue 组件实例 |
TIP
mount 与 shallowMount 的区别:mount 会完整挂载子组件,shallowMount 将子组件替换为 stub。日常测试中 mount 使用较多,因为它能验证组件间的交互行为。
覆盖率说明
Toast 组件测试中可能遇到分支覆盖不完整的情况。例如 setTimeout 回调内的条件分支(showValue 为 false 时的逻辑),可能需要通过额外的 setProps({ modelValue: false }) 来覆盖。
测试的核心目的是验证逻辑正确性,而非强求每一行 100% 覆盖。对于实验性特性(如 Vue 的 defineModel),部分内部代码可能难以通过常规方式测试到,这种情况下关注功能验证即可。
↑